package com.atguigu.gulimall.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.common.constant.OrderConstant;
import com.atguigu.common.constant.SeckillConstant;
import com.atguigu.common.exception.BizErrorCode;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.to.product.SkuInfoTo;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.seckill.feign.CouponFeignService;
import com.atguigu.gulimall.seckill.feign.ProductFeignService;
import com.atguigu.gulimall.seckill.interceptor.LoginInterceptor;
import com.atguigu.gulimall.seckill.service.SeckillService;
import com.atguigu.gulimall.seckill.to.SeckillSessionWithSkuItemsTo;
import com.atguigu.gulimall.seckill.to.SeckillSkuRedisTo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void uploadNewestSessions(Integer days) {
        R r = couponFeignService.getNewestSession(days);
        if (r.isOk()) {
            List<SeckillSessionWithSkuItemsTo> data = r.getData(new TypeReference<List<SeckillSessionWithSkuItemsTo>>() {
            });
            if (data != null && !data.isEmpty()) {
                saveSeckillSessionInfos(data);
                saveSeckillSkuInfos(data);
            }
        }
    }

    @Override
    public List<SeckillSkuRedisTo> getSeckillSkusByTs(long ts, boolean needRandomCode) {
        // 符合当前数据的秒杀场次
        Set<String> keys = stringRedisTemplate.keys(SeckillConstant.SEC_KILL_SESSION_CACHE_PREFIX + "*");
        int beginIndex = SeckillConstant.SEC_KILL_SESSION_CACHE_PREFIX.length();
        if (keys != null) {
            for (String key : keys) {
                String[] dateTuple = key.substring(beginIndex).split("_");
                long startTs = Long.parseLong(dateTuple[0]);
                long endTs = Long.parseLong(dateTuple[1]);
                if (ts >= startTs && ts <= endTs) {
                    // 表明存在此场次数据
                    List<String> data = stringRedisTemplate.opsForList().range(key, -100, 100);
                    // 获取这些session_id/skuId 对应的sku信息
                    BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SEC_KILL_SKU_CACHE_PREFIX);
                    if (data != null) {
                        return Objects.requireNonNull(hashOps.multiGet(data))
                                .stream().map(item -> {
                                    SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
                                    if (!needRandomCode) {
                                        seckillSkuRedisTo.setRandomCode("");
                                    }
                                    return seckillSkuRedisTo;
                                }).collect(Collectors.toList());
                    }
                    // todo 其实无必要
                    break;
                }
            }
        }
        return null;
    }

    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SEC_KILL_SKU_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (keys != null && !keys.isEmpty()) {
            String regex = "\\d_" + skuId;
            for (String key : keys) {
                if (Pattern.matches(regex, key)) {
                    return JSON.parseObject(hashOps.get(key), SeckillSkuRedisTo.class);
                }
            }
        }
        return null;
    }

    @SneakyThrows
    @Override
    public Object killSkuItem(String killId, Long num, String key) {
        // 获取场次是否在秒杀时段
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SEC_KILL_SKU_CACHE_PREFIX);
        String seckillJsonInfo = hashOps.get(killId);
        if (StringUtils.isEmpty(seckillJsonInfo)) {
            log.info("无法获取当前场次的秒杀【{}】信息", killId);
            return BizErrorCode.NOT_EXIST_PRODUCT_ERROR;
        }
        SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(seckillJsonInfo, SeckillSkuRedisTo.class);
        Long startTs = seckillSkuRedisTo.getStartTs();
        Long endTs = seckillSkuRedisTo.getEndTs();
        long currentTs = System.currentTimeMillis();
        if (!(currentTs >= startTs && endTs >= currentTs)) {
            log.info("不在秒杀时间段");
            return BizErrorCode.NO_IN_CURRENT_PERIOD_ERROR;
        }
        // 校验随机码和killId
        if (!key.equals(seckillSkuRedisTo.getRandomCode())) {
            log.info("秒杀商品校验码错误");
            return BizErrorCode.INCORRECT_VERITY_CODE_ERROR;
        }

        String redisKillId = seckillSkuRedisTo.getPromotionId() + "_" + seckillSkuRedisTo.getSkuId();
        // 校验随机码和killId
        if (!killId.equals(redisKillId)) {
            log.info("秒杀商品场次信息错误");
            return BizErrorCode.INCORRECT_SESSION_PRODUCT_ERROR;
        }

        // 校验收否已经购买过/幂等性。只能购买一次
        Long userId = LoginInterceptor.getLoginUserInfo().getId();
        long ttl = endTs - currentTs;
        String idempotenceKey = SeckillConstant.SEC_KILL_IDEMPOTENCE_LOCK_CACHE_NAME + userId + "_" + killId;
        Boolean notPurchased = stringRedisTemplate.opsForValue().setIfAbsent(idempotenceKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
        if (Boolean.FALSE.equals(notPurchased)) {
            log.info("商品购买次数达到限制");
            return BizErrorCode.REACHED_LIMIT_TIMES_ERROR;
        }

        // 尝试秒杀
        RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SEC_KILL_STOCK_SEMAPHORE_CACHE_PREFIX + key);

        boolean success = semaphore.tryAcquire(Math.toIntExact(num), 100, TimeUnit.MILLISECONDS);
        if (!success) {
            log.info("手滑了，没抢到");
            return BizErrorCode.TOO_SLOW_ERROR;
        }

        // 生成订单号
        String orderSn = IdWorker.getTimeId();

        SeckillOrderTo order = new SeckillOrderTo();
        order.setOrderSn(orderSn);
        order.setPromotionSessionId(seckillSkuRedisTo.getPromotionId());
        order.setSkuId(seckillSkuRedisTo.getSkuId());
        order.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
        order.setNum(num);
        order.setMemberId(userId);
        // todo 发送下单消息给 MQ
        rabbitTemplate.convertAndSend(OrderConstant.ORDER_EVENT_EXCHANGE_NAME, SeckillConstant.SECKILL_ORDER_ROUTE_KEY_NAME, order);
        return orderSn;
    }

    private void saveSeckillSkuInfos(List<SeckillSessionWithSkuItemsTo> data) {
        for (SeckillSessionWithSkuItemsTo item : data) {
            long startTs = item.getStartTime().getTime();
            long endTs = item.getEndTime().getTime();
            String key = SeckillConstant.SEC_KILL_SESSION_CACHE_PREFIX + startTs + "_" + endTs;
            if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(key))) {
                // 缓存中不存在此数据
                List<String> skuIds = item.getRelationItems().stream().map(ri -> ri.getPromotionSessionId() + "_" + ri.getSkuId()).collect(Collectors.toList());
                stringRedisTemplate.opsForList().leftPushAll(key, skuIds);
            }

        }
    }

    private void saveSeckillSessionInfos(List<SeckillSessionWithSkuItemsTo> data) {
        for (SeckillSessionWithSkuItemsTo session : data) {
            BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SeckillConstant.SEC_KILL_SKU_CACHE_PREFIX);
            for (SeckillSessionWithSkuItemsTo.SeckillSkuRelationTo relationItem : session.getRelationItems()) {

                String cacheKey = relationItem.getPromotionSessionId() + "_" + relationItem.getSkuId();

                // 只有当缓存中没有此场次的商品信息时，才设置数据，商品sku数据与信号量只设置一次
                if (Boolean.FALSE.equals(hashOps.hasKey(cacheKey))) {
                    SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                    // 设置关联秒杀信息
                    BeanUtils.copyProperties(relationItem, seckillSkuRedisTo);
                    // 设置sku详细信息
                    R r = productFeignService.skuInfo(relationItem.getSkuId());
                    if (r.isOk()) {
                        SkuInfoTo skuInfo = r.getData("skuInfo", new TypeReference<SkuInfoTo>() {
                        });
                        seckillSkuRedisTo.setSkuInfo(skuInfo);
                    }

                    // 设置秒杀开始结束时间
                    seckillSkuRedisTo.setStartTs(session.getStartTime().getTime());
                    seckillSkuRedisTo.setEndTs(session.getEndTime().getTime());

                    // 设置随机码
                    String token = UUID.randomUUID().toString().replace("-", "");
                    seckillSkuRedisTo.setRandomCode(token);

                    // 设置分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SEC_KILL_STOCK_SEMAPHORE_CACHE_PREFIX + token);

                    semaphore.trySetPermits(relationItem.getSeckillCount().intValue());

                    // 保存数据
                    String jsonData = JSON.toJSONString(seckillSkuRedisTo);
                    hashOps.put(cacheKey, jsonData);
                }
            }
        }
    }
}
